<?php
/** 
 * Multi Tree Behaviour Class
 * 来源：https://github.com/cyberthom/MultiTree-Behavior/blob/master/models/behaviors/multi_tree.php
 * 实例：https://github.com/cyberthom/MultiTree-Behavior
 */
class TreeBehavior extends ModelBehavior
{
	var $_defaults = array (
			// Field names
			'parent' => 'parent_id','left' => 'lft','right' => 'rght','root' => 'root_id',
			// optional,allow multiple trees per table
			'level' => 'level',			// optional, cache levels
			'dependent' => false,'callbacks' => true,			// Other
			'recursive' => - 1,			// Private
			'__treeFields' => array (),'__move' => false,'__delete' => false 
	);
	/**
	 * undocumented function
	 *
	 * @access public
	 * @return void
	 *
	 */
	function setup(&$Model, $config = array())
	{
		// Merge config with defaults
		if (! is_array ( $config ))
		{
			$config = array ();
		}
		$this->settings [$Model->alias] = array_merge ( $this->_defaults, $config );
		// __treeFields
		if (empty ( $this->settings [$Model->alias] ['__treeFields'] ))
		{
			$this->settings [$Model->alias] ['__treeFields'] = array (
					$this->settings [$Model->alias] ['parent'],$this->settings [$Model->alias] ['left'],$this->settings [$Model->alias] ['right'] 
			);
			if (! empty ( $this->settings [$Model->alias] ['root'] ))
			{
				// if (
				// !$Model->hasField($this->settings[$Model->alias]['root']) )
				$this->settings [$Model->alias] ['__treeFields'] [] = $this->settings [$Model->alias] ['root'];
			}
			if (! empty ( $this->settings [$Model->alias] ['level'] ))
			{
				// if (
				// !$Model->hasField($this->settings[$Model->alias]['level']) )
				$this->settings [$Model->alias] ['__treeFields'] [] = $this->settings [$Model->alias] ['level'];
			}
		}
	}
	/**
	 *
	 *
	 *
	 * undocumented function
	 *
	 * @access public
	 * @return boolean
	 */
	function beforeSave(&$Model)
	{
		extract ( $this->settings [$Model->alias] );
		// Are we about to create or edit?
		$creating = empty ( $Model->id );
		// Check if we need to perform changes to the tree
		if (isset ( $Model->data [$Model->alias] [$parent] ))
		{
			// Get node
			if (! $creating && ($node = $this->_node ( $Model, $Model->id )) === false)
			{
				return false;
			}
			// Accept array with position information
			$position = 'lastChild';
			if (is_array ( $Model->data [$Model->alias] [$parent] ))
			{
				if (array_key_exists ( 'destination', $Model->data [$Model->alias] [$parent] ) && array_key_exists ( 'position', $Model->data [$Model->alias] [$parent] ))
				{
					$position = $Model->data [$Model->alias] [$parent] ['position'];
					$Model->data [$Model->alias] [$parent] = $Model->data [$Model->alias] [$parent] ['destination'];
				} else
				{
					$Model->data [$Model->alias] [$parent] = reset ( $Model->data [$Model->alias] [$parent] );
				}
			}
			// Any parent changes?
			if ($creating || $Model->data [$Model->alias] [$parent] != $node [$parent])
			{
				// Check if parent axists
				if (! empty ( $Model->data [$Model->alias] [$parent] ) && ($destNode = $this->_node ( $Model, $Model->data [$Model->alias] [$parent] )) === false)
				{
					$Model->invalidate ( $parent, 'Parent does not exist' );
					return false;
				}
				// Mark for moving
				$this->settings [$Model->alias] ['__move'] = array (
						'parent' => $Model->data [$Model->alias] [$parent],'position' => $position 
				);
			}
		} else if (! empty ( $root ) && isset ( $Model->data [$Model->alias] [$root] ))
		{
			// Get node
			if (! $creating && ($node = $this->_node ( $Model, $Model->id )) === false)
			{
				return false;
			}
			// Any root changes?
			if ($creating || $Model->data [$Model->alias] [$root] != $node [$root])
			{
				// Mark for moving
				$this->settings [$Model->alias] ['__move'] = array (
						'root' => $Model->data [$Model->alias] [$root] 
				);
			}
		} else if ($creating)
		{
			$this->settings [$Model->alias] ['__move'] = null;
		}
		// Don't allow manually changing left, right etc.
		$Model->data [$Model->alias] = array_diff_key ( $Model->data [$Model->alias], array_flip ( $__treeFields ) );
		
		return true;
	}
	
	/**
	 * undocumented function
	 *
	 * @access public
	 * @return boolean
	 *
	 */
	function afterSave(&$Model, $created)
	{
		if ($this->settings [$Model->alias] ['__move'] !== false)
		{
			$this->move ( $Model, $Model->id, $this->settings [$Model->alias] ['__move'] );
			$this->settings [$Model->alias] ['__move'] = false;
		}
	}
	/**
	 * * undocumented function * * @return void *
	 */
	function beforeDelete(&$Model, $cascade)
	{
		$this->settings [$Model->alias] ['__delete'] = $this->_node ( $Model, $Model->id );
		return true;
	}
	/**
	 * * undocumented function * * @access public * @return boolean *
	 */
	function afterDelete(&$Model)
	{
		if ($this->settings [$Model->alias] ['__delete'] !== false)
		{
			$this->_removeFromTree ( $Model, $this->settings [$Model->alias] ['__delete'], $this->settings [$Model->alias] ['dependent'] );
			$this->settings [$Model->alias] ['__delete'] = false;
		}
	}
	
	/**	 
	 * undocumented function	 
	 *  
	 * @access public	 
	 * @return boolean	 
	 * */	
	function move(&$Model, $id = null, $dest = null, $position = 'lastChild')
	{
		if (! $id && $Model->id)
		{
			$id = $Model->id;
		}
		extract ( $this->settings [$Model->alias] );
		if (! is_array ( $dest ))
		{
			$dest = array (
					'parent' => $dest 
			);
		} else if (array_key_exists ( 'position', $dest ))
		{
			$position = $dest ['position'];
		}
		// Get node
		if (($node = $this->_node ( $Model, $id )) === false)
		{
			return false;
		}
		$oldNode = $node;
		$invalid = (empty ( $oldNode [$left] ) || empty ( $oldNode [$right] ));
		// Start transaction
		$Model->getDataSource ()->begin ( $Model );
		// Fake loop allowing us to jump to the end on failure
		while ( $commit = true )
		{
			// Get node size
			if ($invalid)
			{
				$node [$left] = 1;
				$node [$right] = 2;
				// $node[$parent] = null;
				// if ( !empty($root) )
				// $node[$root] = null;
			}
			$treeSize = $node [$right] - $node [$left] + 1;
			// Are we moving to another node?
			if (! empty ( $dest ['parent'] ))
			{
				// Get destination node
				if (($destNode = $this->_node ( $Model, $dest ['parent'] )) === false)
				{
					// return false;
					$Model->invalidate ( $parent, 'Parent does not exist' );
					$commit = false;
					break;
				}
				// Do not allow to move a node to or inside itself
				if (! $invalid && (empty ( $root ) || $node [$root] == $destNode [$root]) && ($destNode [$left] >= $node [$left] && $destNode [$right] <= $node [$right]))
				{
					// return false;
					$Model->invalidate ( $parent, 'Destination node is within source tree' );
					$commit = false;
					break;
				}
				// Set beginning of shift range
				switch ($position)
				{
					case 'prevSibling' :
					case 'before' :
						$node [$parent] = $destNode [$parent];
						if (! empty ( $level ))
							$node [$level] = $destNode [$level];
						$start = $destNode [$left];
						break;
					case 'nextSibling' :
					case 'after' :
						$node [$parent] = $destNode [$parent];
						if (! empty ( $level ))
							$node [$level] = $destNode [$level];
						$start = $destNode [$right] + 1;
						break;
					case 'firstChild' :
						$node [$parent] = $destNode [$Model->primaryKey];
						// Same as $dest['parent']
						if (! empty ( $level ))
							$node [$level] = $destNode [$level] + 1;
						$start = $destNode [$left] + 1;
						break;
					case 'lastChild' :
					default :
						$node [$parent] = $destNode [$Model->primaryKey];
						// Same as $dest['parent']
						if (! empty ( $level ))
							$node [$level] = $destNode [$level] + 1;
						$start = $destNode [$right];
				}
				if (! empty ( $root ))
					$node [$root] = $destNode [$root];
					// Create gap for node in target tree
				if (($commit = $this->_shift ( $Model, $start, $treeSize, @$destNode [$root] )) === false)
					break;
					// Refresh node record (might have been affected by previous
				// shift)
					// $node = $this->_node(&$Model, $id); // We can save us
				// this query with the following:
				if (($affectedLeft = (! $invalid && (empty ( $root ) || $node [$root] == $destNode [$root]) && $node [$left] >= $start)) !== false)
					$node [$left] += $treeSize;
				if (($affectedRight = (! $invalid && (empty ( $root ) || $node [$root] == $destNode [$root]) && $node [$right] >= $start)) !== false)
					$node [$right] += $treeSize;
			} else if (empty ( $root ))
			{ 
				// Move to the end of  new tree
				$node [$parent] = null;
				if (! empty ( $level ))
					$node [$level] = 0;
				$start = $this->_max ( $Model, $right ) + 1;
			} else if (! empty ( $dest ['root'] ))
			{
				// Move to the end of tree
				$node [$root] = $dest ['root'];
				$node [$parent] = null;
				if (! empty ( $level ))
					$node [$level] = 0;
				$start = $this->_max ( $Model, $right, array (
						$Model->escapeField ( $root ) => $dest ['root'] 
				) ) + 1;
			} else if (isset ( $dest ['root'] ) && ! empty ( $node [$root] ))
			{
				// Move to the end of tree
				// $node[$root] = $node[$root]; // I know..
				$node [$parent] = null;
				if (! empty ( $level ))
					$node [$level] = 0;
				$start = $this->_max ( $Model, $right, array (
						$Model->escapeField ( $root ) => $node [$root] 
				) ) + 1;
			} else
			{
				// Move to the end of new tree
				$node [$root] = $id;
				$node [$parent] = null;
				if (! empty ( $level ))
					$node [$level] = 0;
				$start = 1;
			}
			if (! $invalid && $treeSize > 2)
			{
				// Move node into that gap (Save new left, right, root and
				// level)
				$diff = $start - $node [$left];
				$levelDiff = ! empty ( $level ) ? $node [$level] - $oldNode [$level] : 0;
				if (($commit = $this->_shiftRange ( $Model, $node [$left], $node [$right], $diff, @$oldNode [$root], @$node [$root], @$levelDiff )) === false)
					break;
					// Save new parent
				if (($commit = ($Model->save ( $node, array (
						'callbacks' => false,'validate' => false,'fieldList' => array (
								$parent 
						) 
				) ) !== false)) === false)
					break;
			} else
			{
				// Move node into that gap (Save new left, right, root, parent
				// and level)
				$diff = $start - $node [$left];
				$data = $node; // Create new array, otherwise we affect range of
				               // shift() below
				$data [$left] += $diff;
				$data [$right] += $diff;
				if (($commit = ($Model->save ( $data, array (
						'callbacks' => false,'validate' => false,'fieldList' => $__treeFields 
				) ) !== false)) === false)
					break;
			}
			// Remove gap created while removing node from source tree
			if (! $invalid)
			{
				if (($commit = $this->_shift ( $Model, $node [$left], - $treeSize, @$oldNode [$root] )) === false)
					break;
			}
			// We don't want this to actually loop
			break;
		}		
																																		// Commit		
																																		if ( $commit ) {			$Model->getDataSource()->commit($Model);		} else {			$Model->getDataSource()->rollback($Model);		}		return $commit;	}
}
?>